Harden RAG answer prompt against source-text injection (measurement-first)#366
Merged
Conversation
Measurement-first LLM-pipeline review. Live eval:rag (44 cases) priced at current gpt-5.5 rates confirms the fast->quality-gate->strong cascade still beats routing straight to strong: strong costs ~1.8x fast at +13s median latency while fast grounds 100% of its queries. So no routing-threshold, timeout, reasoning-effort, or citation-schema changes are made — the citation enum constraint (answerJsonOutputSchemaForResults) and reasoning-effort latency cap already exist, and prompt caching is already configured. Implements the ranked prompt-level mitigations from docs/rag-injection-threat-model.md (see new section 6): - answerInstructions gains an explicit provenance/untrusted-data boundary so the evidence fence is a declared trust boundary the model is told to obey as data, never as instructions (mitigation #1). Zero false-positive risk to clinical content — it changes instructions, not source text. - buildRagSourceBlock now neutralizes the previously-RAW prompt-facing fields (title, file_name, image caption/tableTitle/tableLabel, index warnings) (Vectors B/C, mitigation #2). - The fence escaper is case-insensitive and evidence text is neutralized after glyph normalization, defusing lowercase forged sentinels and zero-width / homoglyph obfuscation (mitigation #3, INJ-3/12). - Every derived evidence field (synopsis, adjacent context, table facts, memory cards, images, section path) is escaped in place so a forged close-then-reopen sentinel cannot straddle the real fence (Vector E, mitigation #5). Escaping in place rather than wrapping each field avoids the ~940 input-tokens/answer the per-field wrapper added, which tipped near-timeout strong-route answers over budget. - Answer prompt-cache key bumped v17 -> v18 (cached instruction prefix changed). The longer instruction prefix now crosses OpenAI's prompt-cache minimum, so cached-input share rises from ~4% to ~40-80% and cost/answer is neutral-to-lower despite the added text. Adds tests/rag-injection.test.ts (deterministic INJ-3/4/6/12 + neutralizer / escaper regression guards). Answer-text-level INJ cases need a seeded-chunk generation harness and are deferred. Gates: verify:cheap green (1261 tests); eval:quality --rag-only grounded- supported 0.9333 (= baseline); eval:rag grounded-supported at parity within run-to-run noise; retrieval untouched. No retrieval RPC / DB / migration changes; no source-governance metadata added to ranking. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdSevAcjHqHt4ezZkWYbE7
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
BigSimmo
marked this pull request as ready for review
July 8, 2026 04:29
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
BigSimmo
enabled auto-merge
July 8, 2026 04:29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
eval:rag(44 cases) priced at current gpt-5.5 rates ($5 / $0.50 cached / $30 per 1M in/cached/out) +rag_retrieval_logstelemetry (306 rows) validate the fast→quality-gate→strong cascade against the current model generation, so no routing/timeout/schema churn is made. Fast and strong are the same model (gpt-5.5) differentiated only by reasoning effort; strong costs 1.8× fast ($0.101 vs $0.056) at +13s median latency while fast grounds 100% of the queries routed to it. Routing everything to a strong model would roughly double cost and latency on the ~40% of traffic fast handles perfectly, for no quality gain.answerJsonOutputSchemaForResults) already closes the master plan's "where practical" gap;strongReasoningEffortForQueryClassalready caps effort except for safety-critical classes; prompt caching is already configured. Only change here is the answer cache keyv17→v18(the cached instruction prefix changed).docs/rag-injection-threat-model.md(new §6): provenance/untrusted-data boundary inanswerInstructions; neutralize the previously-RAWtitle/file_name/imagecaption/tableTitle/tableLabel/index-warning fields; case-insensitive fence escaper + neutralize-after-glyph-normalization; escape forged fence sentinels in every derived field in place (closes Vector E without the ~940 tokens/answer a per-field wrapper added). Newtests/rag-injection.test.tslands the deterministic INJ-3/4/6/12 cases + neutralizer/escaper regression guards.Cost-per-answer by route (BEFORE / main, 44 cases, gpt-5.5 pricing)
Before / after (44-case
eval:rag)eval:quality --rag-only: 28/30)The 1-case
eval:ragdelta is within run-to-run noise: the two residual "regressions" are strong-route answers at 44s and 55s latency (timeout fallbacks on cases already latency-fragile on main), one baseline-failing clozapine case actually recovered to grounded, andeval:quality --rag-onlyon the same code matched baseline at 28/30. No content-level grounding loss from the hardening.Verification
npm run verify:cheap— green (1261 tests, lint 0 errors, typecheck clean)npm run verify:ui— n/a (no UI/routing/styling change)npm run verify:release— not run (no release/handoff claim)npm run format:check— changed files pass Prettiernpm run eval:retrieval:quality— retrieval is not touched (prompt-assembly + content-sanitizer only); baseline onmainrecorded 36/36, failed_cases=0 before any change. Not re-run because ranking/selection/chunking/scoring are unchanged.npm run eval:rag(full) +npm run eval:quality -- --rag-only— grounded-supported 0.9333 (= baseline), invalid-citation 0. The non-zero citation/numeric/latency threshold flags in the quality report are pre-existing on main (identical failing cases in the pre-editeval:ragbaseline:clozapine-anc-withhold-threshold,community-admission,illegal-substances), not introduced here.npm run check:production-readiness— n/a (no clinical-workflow/privacy/env/Supabase/governance/deployment change)npm run check:deployment-readiness— n/aClinical Governance Preflight
Clinical KB Database(sjrfecxgysukkwxsowpy) — no Supabase/env/RPC/migration changeNotes
rag_retrieval_logsdoes not persist per-answer route/model/token usage, so answer cost is only measurable viaeval:rag(a Phase-7 metric worth adding separately).🤖 Generated with Claude Code
https://claude.ai/code/session_01VdSevAcjHqHt4ezZkWYbE7
Generated by Claude Code